package com.openwater.network;

import java.io.File;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.os.Environment;
import android.util.Log;

import com.openwater.network.DownloadThread.OnProcessListener;
import com.openwater.tools.StringUtil;

/**
 * An asynchronous multi-threaded breakpoint continuingly download program
 * 
 * @author fuxp
 * @version 1.0 At 2014-4-6
 */
public class FileDownloader {

    private static final String TAG = FileDownloader.class.getName();

    private static final String BASE_DIR = Environment.getExternalStorageDirectory()
            .getAbsolutePath();

    /** default store directory */
    private static final String DEFAlUT_STORE_DIR = BASE_DIR + File.separator + "MyDOWNLOAD";

    /** if download failed how many times will retry (every thread) */
    public static final int RETRY_TIMES = 3;

    /**
     * download progress listener ,if your care download progress should
     * register this listener <BR>
     * NOTICE: that will NOT call back in UI thread
     * 
     * @author fuxp
     */
    public interface ProgressListener {
        /**
         * 
         * @param size
         *            current download size(KB)
         * @param totalSize
         *            remote file size(KB)
         * @param speed
         *            speed(KB/s)
         * @param remainTime
         *            remaining time(s)
         */
        void onDownloadProcess(int size, int totalSize, float speed, int remainTime);

        void onDownloadFail(String fileName);
    }

    private final Object mLock = new Object();
    private final Map<Integer, Integer> mDataMap = new HashMap<Integer, Integer>();

    private boolean mPause = false;
    private boolean mStop = false;
    private String mUrl = null;

    /** remote original file size */
    private int mFileSize = 0;

    /** download file size */
    private int mDownloadSize = 0;

    /** download threads */
    private DownloadThread[] mThreads = null;

    /** download file name */
    private String mFileName = null;

    private String mSavePath = null;

    private ProgressListener mProgressListener = null;

    public String getFileName() {
        return mFileName;
    }

    public void setFileName(String fname) {
        mFileName = fname;
    }

    public int getThreadSize() {
        return mThreads.length;
    }

    public boolean isFinish() {
        return mDownloadSize == mFileSize;
    }

    public boolean isPause() {
        synchronized (mLock) {
            return mPause;
        }
    }

    public void setPause(boolean p) {
        synchronized (mLock) {
            mPause = p;
            if (mPause) {
                download();
            }
        }
    }

    public boolean isStop() {
        synchronized (mLock) {
            return mStop;
        }
    }

    public void stop() {
        synchronized (mLock) {
            mStop = true;
        }
    }

    public String getUrl() {
        return mUrl;
    }

    /**
     * @return remote file size will be download(KB)
     */
    public int getFileSize() {
        return mFileSize / 1024;
    }

    public FileDownloader(String downloadUrl) {
        this(downloadUrl, null, null, 3, null);
    }

    public FileDownloader(String downloadUrl, String path, ProgressListener listener) {
        this(downloadUrl, path, null, 3, listener);
    }

    public FileDownloader(String downloadUrl, String path, int threadNum, ProgressListener listener) {
        this(downloadUrl, path, null, threadNum, listener);
    }

    public FileDownloader(String downloadUrl, String path, String fileName, int threadNum,
            ProgressListener listener) {
        mUrl = downloadUrl;
        mFileName = fileName;
        mProgressListener = listener;
        mThreads = new DownloadThread[threadNum];
        mSavePath = path == null ? DEFAlUT_STORE_DIR : path;
    }

    private static ExecutorService mExecutors = Executors.newSingleThreadExecutor();

    /**
     * start the download task<BR>
     * NOTICE:it's invoke into sub-thread to Execute
     * 
     * @param l
     * @see #download()
     */
    public void download(ProgressListener l) {
        mProgressListener = l;
        download();
    }

    /**
     * start the download task<BR>
     * NOTICE:it's invoke into sub-thread to Execute
     * 
     * @see #download(ProgressListener)
     */
    public void download() {
        if (mExecutors.isShutdown() || mExecutors.isTerminated()) {
            Log.e("error", "mExecutors is Shutdown or Terminated!!! # can not start download");
        } else {
            mExecutors.execute(new Runnable() {
                @Override
                public void run() {
                    if (perpare()) {
                        startDownload();
                    }
                }
            });
        }
    }

    private boolean perpare() {
        int retryTimes = 0;
        String requestMothed = "POST";
        while (retryTimes < RETRY_TIMES) {
            try {
                final URL url = new URL(mUrl);
                final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setConnectTimeout(5 * 1000);
                conn.setRequestMethod(requestMothed);
                conn.setRequestProperty("Accept",
                        "image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
                                + "application/x-shockwave-flash, application/xaml+xml, "
                                + "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
                                + "application/x-ms-application, application/vnd.ms-excel, "
                                + "application/vnd.ms-powerpoint, application/msword, */*");
                conn.setRequestProperty("Accept-Language", "zh-CN");
                conn.setRequestProperty("Referer", mUrl);
                conn.setRequestProperty("Charset", "UTF-8");
                conn.setRequestProperty("User-Agent",
                        "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0;"
                                + " .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30;"
                                + " .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
                conn.setRequestProperty("Connection", "Keep-Alive");
                // conn.setRequestProperty("content-length", "");
                conn.connect();
                // print some logs
                printResponseHeader(conn);
                final int retCode = conn.getResponseCode();
                Log.d(TAG,
                        "ResponseCode: " + retCode + " ResponseMessage : "
                                + conn.getResponseMessage());
                if (retCode == HttpURLConnection.HTTP_OK
                        || retCode == HttpURLConnection.HTTP_ACCEPTED) {
                    // get remote file size
                    mFileSize = conn.getContentLength();
                    Log.d(TAG, "remote file size : " + mFileSize);
                    if (mFileSize <= 0) {
                        Log.d(TAG, "illegality file size : " + mFileSize);
                        if (mProgressListener != null) {
                            mProgressListener.onDownloadFail(mFileName);
                        }
                        return false;
                    }
                    mFileName = getFileName(conn);

                    if (!DDBHelper.exists(mUrl)) {
                        DDBHelper.addDownloadFile(mFileName, mSavePath,
                                String.valueOf(System.currentTimeMillis()), mUrl, 1);
                    }

                    // The already down-loaded data length in each thread
                    final Map<Integer, Integer> logdata = DDBHelper
                            .getMultithreadsDownloadInfor(mUrl);
                    if (logdata.size() > 0) {
                        for (Map.Entry<Integer, Integer> entry : logdata.entrySet())
                            mDataMap.put(entry.getKey(), entry.getValue());
                    }
                    // calculate How much is complete
                    if (mDataMap.size() == mThreads.length) {
                        for (int i = 0; i < mThreads.length; i++) {
                            mDownloadSize += mDataMap.get(i);
                        }
                    }
                    conn.disconnect();
                    return true;
                } else {
                    retryTimes++;
                    requestMothed = retryTimes % 2 == 0 ? "POST" : "GET";
                    Log.d(TAG, "retryTimes : " + retryTimes + " server no response : " + mUrl
                            + "  " + mFileName);
                    conn.disconnect();
                }
            } catch (Exception e) {
                e.printStackTrace();
                retryTimes++;
                requestMothed = retryTimes % 2 == 0 ? "POST" : "GET";
                Log.d(TAG,
                        "retryTimes : " + retryTimes + "don't connection this url : "
                                + e.getMessage());
            }
        }
        if (mProgressListener != null) {
            mProgressListener.onDownloadFail(mFileName);
        }
        return false;
    }

    private int startDownload() {
        try {
            final File dir = new File(mSavePath);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            final File saveFile = new File(dir, mFileName);
            final RandomAccessFile randOut = new RandomAccessFile(saveFile, "rw");
            randOut.setLength(mFileSize);
            randOut.close();

            // initialized every thread download size is 0
            if (mDataMap.size() != mThreads.length) {
                mDataMap.clear();
                for (int i = 0; i < mThreads.length; i++) {
                    mDataMap.put(i, 0);
                }
            }

            final URL url = new URL(mUrl);
            final int block = mFileSize / mThreads.length;
            // start threads to download
            mStartTime = System.currentTimeMillis();
            for (int i = 0; i < mThreads.length; i++) {
                final int downLength = mDataMap.get(i);
                final int offset = i == 0 ? 0 : 1;
                final int moreSize = i == mDataMap.size() - 1 ? mFileSize % mThreads.length : 0;
                if (downLength < block + moreSize && mDownloadSize < mFileSize) {
                    mThreads[i] = new DownloadThread(this, url, saveFile, block * i + downLength
                            + offset, block * (i + 1) + moreSize, i);
                    mThreads[i].setOnProcessListener(mProcessListener);
                    mThreads[i].setPriority(4);
                    mThreads[i].start();
                    Log.d(TAG, "task thread " + i + " : from " + (block * i + downLength + offset)
                            + " to " + (block * (i + 1) + moreSize));
                } else {
                    mThreads[i] = null;
                }
            }
        } catch (Exception e) {
            mStartTime = 0;
            Log.d(TAG, "file download fail");
        }
        return mDownloadSize;
    }

    private long mStartTime = 0;
    private float mSpeed = 0;

    /**
     * download real-time speed(KB/s)
     * 
     * @return
     */
    public float getSpeed() {
        synchronized (mLock) {
            return mSpeed;
        }
    }

    public float getPersent() {
        synchronized (mLock) {
            return mDownloadSize * 1.0f / mFileSize;
        }
    }

    public int getDownloadSize() {
        synchronized (mLock) {
            return mDownloadSize;
        }
    }

    public long getStartTime() {
        return mStartTime;
    }

    private long timeforSpeed = 0;
    private int sizeforSpeed = 0;
    private final static int UNITY_SPEED_S = 2;
    private OnProcessListener mProcessListener = new OnProcessListener() {

        @Override
        public void onProcess(int threadId, int offset, int length) {
            synchronized (mLock) {
                mDownloadSize += offset;
                if (System.currentTimeMillis() - timeforSpeed >= UNITY_SPEED_S * 1000) {
                    timeforSpeed = System.currentTimeMillis();
                    mSpeed = sizeforSpeed / 1024.0f / UNITY_SPEED_S;
                    sizeforSpeed = 0;
                } else {
                    sizeforSpeed += offset;
                }
                float percent = mDownloadSize * 1.0f / mFileSize;
                Log.d(TAG,
                        "threadId: " + threadId + " ,offset:" + offset + "  DownloadSize : "
                                + mDownloadSize + ", FileSize : " + mFileSize + " percent:"
                                + StringUtil.formatPercent(percent));
                if (mDownloadSize == mFileSize) {
                    Log.d(TAG, "task " + mUrl + " is finish");
                    DDBHelper.updateDownloadFile(mUrl, 0);
                    DDBHelper.deleteMultithreadsDownloadInfor(mUrl);
                }
                if (mProgressListener != null) {
                    mProgressListener.onDownloadProcess(mDownloadSize / 1024, mFileSize / 1024,
                            mSpeed, (int) ((mFileSize - mDownloadSize) / mSpeed));
                }
            }
        }

        @Override
        public void onFail(int threadId, int length) {
            synchronized (mLock) {
                Log.d(TAG, "thread : " + threadId + " is download fail");
                mDataMap.put(threadId, length);
                DDBHelper.updateMultithreadsDownloadInfor(mUrl, threadId, length);
                if (mProgressListener != null) {
                    mProgressListener.onDownloadFail(mFileName);
                }
            }
        }
    };

    private String getFileName(HttpURLConnection conn) {
        if (mFileName != null)
            return mFileName;
        mFileName = mUrl.substring(mUrl.lastIndexOf('/') + 1);
        if (mFileName == null || "".equals(mFileName.trim())) {
            for (int i = 0;; i++) {
                String mine = conn.getHeaderField(i);
                if (mine == null)
                    break;
                if ("content-disposition".equals(conn.getHeaderFieldKey(i).toLowerCase())) {
                    Matcher m = Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase());
                    if (m.find())
                        return m.group(1);
                }
            }
            // use UUID as a default name
            mFileName = UUID.randomUUID() + ".tmp";
        }
        return mFileName;
    }

    private static void printResponseHeader(HttpURLConnection http) {
        Map<String, String> header = getHttpResponseHeader(http);
        for (Map.Entry<String, String> entry : header.entrySet()) {
            String key = entry.getKey() != null ? entry.getKey() : "";
            Log.d(TAG, key + " : " + entry.getValue());
        }
    }

    public static Map<String, String> getHttpResponseHeader(HttpURLConnection http) {
        Map<String, String> header = new LinkedHashMap<String, String>();
        for (int i = 0;; i++) {
            String mine = http.getHeaderField(i);
            if (mine == null)
                break;
            header.put(http.getHeaderFieldKey(i), mine);
        }
        return header;
    }

    public String getStorePath() {
        return mSavePath;
    }

}
